Skip to content

[OMNIML-6062] Dequantize block-FP8 checkpoints as they load expert-parallel - #2743

Open
shengliangxu wants to merge 25 commits into
shengliangx/hfep-6b-ep-dense-shardingfrom
shengliangx/hfep-6c-fp8-dequant
Open

shengliangxu wants to merge 25 commits into
shengliangx/hfep-6b-ep-dense-shardingfrom
shengliangx/hfep-6c-fp8-dequant

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Note

The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [11/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.

Base: #2742. The diff below is this PR's alone, and it merges after #2742.

Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.

Series layout

What does this PR do?

Type of change: New feature

A native block-FP8 checkpoint, such as DeepSeek-V3, is dequantized to bf16 while it loads expert-parallel, through transformers' FineGrainedFP8Config(dequantize=True). Quantizers attach to bf16 weights.

  • A block-FP8 checkpoint keeps its dense part replicated (moved here from [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742, where block FP8 did not load at all): DCP, which loads a sharded dense part, cannot dequantize it: it reads the checkpoint as stored. Left to decide, the plan ([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737) replicates it and says so; an explicit fsdp is refused by the decision, naming MODELOPT_FSDP_SIZE=1, and by the loader.

  • Per-tensor FP8 is refused. AutoFP8's per-tensor checkpoints name the same quant_method without a weight_block_size; transformers' dequant would read them as 128x128 blocks and load their weights unscaled, without an error. The load refuses them, before anything loads, as fp8-per-tensor, naming the weight_block_size they lack. A config handed in as transformers' FP8 config object, which fills in a 128x128 block, is read against the checkpoint's own config.json, and refused when the path holds none to read (a Hub ID the load has not made local yet). A tiny per-tensor FP8 Llama, loaded that way on transformers 5.18:

    Weights Largest absolute value
    original 0.079
    loaded as block FP8 448

On transformers older than 5.10.1, the loader also patches that dequantization in three ways:

  • Partial last block. Stock dequantization infers the block size by dividing the weight shape by the scale grid's, so it raises on a weight whose last 128x128 block is partial, such as DeepSeek-V3's kv_a_proj_with_mqa: (576, 7168) with a (5, 56) grid. It also raises on an empty scale grid. The patch tiles each block's scale and crops to the weight, using the checkpoint's weight_block_size.
  • Output dtype. Stock dequantization does not take the destination dtype. The patch emits the destination dtype the loader passes, else the model's dtype.
  • Routed experts. The FP8 quantizer prepends its dequantization to every weight converter, so the fused-experts converter no longer starts with a MergeModulelist. The loader then assigns no per-expert index, and the expert-parallel shard slices each expert's rows instead of whole experts: a DeepSeek-V3 gate_up_proj loads as (256, 32, 7168) and fails calibration. The patch fuses the dequantization into the merge, so the converter starts with a MergeModulelist again and runs as upstream intends.

The patch installs once and survives a re-imported module. From transformers 5.10.1 it keeps upstream's dequantization, which takes the destination dtype and decodes E8M0 scales, and only pads a weight with a partial last block to whole blocks for it, in its own fp8 dtype, since upstream still infers the block from the scale grid: (448, 7168) under a (4, 56) grid would otherwise be scaled in blocks of 112 rows.

The pre-5.10.1 dequantization scales its own fp32 copy of the weight in place, through a view of its whole column blocks, against scales tiled over the rows only, so no full-size fp32 tile is built. It is rebound only once the expert converter's fuse is in, so a refused patch leaves transformers as it was. Against scaling by a full-size fp32 tile, on CPU with one thread:

Weight Peak memory growth: full tile Peak memory growth: row-tiled Time: full tile Time: row-tiled
(18432, 7168) 1014 MiB (8.0 B/elem) 759 MiB (6.0 B/elem) 1.13 s 0.89 s
(7168, 2048) 116 MiB 88 MiB 127 ms 99 ms

Usage

Automatic: load_and_prepare_ep reads the checkpoint's quantization_config and picks this path for quant_method: fp8 with a weight_block_size.

Testing

  • New tests/unit/torch/utils/test_fp8_dequant_patch.py, on CPU:
    • the patched dequant against a blockwise reference, a weight narrower than one block included;
    • block size from the scale grid or the DeepSeek default;
    • the requested output dtype;
    • an empty scale grid refused for a non-empty weight, rather than passing it through unscaled;
    • idempotent installs that keep the latest dtype;
    • no double-wrapping of a re-imported module;
    • a clean undo;
    • transformers 5.10.1 keeping its own dequant, padded for every partial-block weight, a grid-divisible one included ((192, 256) on a (2, 2) grid);
    • the fused expert converter, run on a real fused-experts converter: it starts with a MergeModulelist, and per-expert block-FP8 weights come out as the blockwise reference. It fails without the fusion, and if the fused op skips the dequantization or the merge. Skipped on transformers 5.5, which has no update_weight_conversions.
  • Mutation-checked: each test fails on at least one deliberate break of the code.
  • The block-size cases join the blockwise-reference table, and dtype and version cases that took the same branch as a kept one are dropped.
  • A refused fuse leaves transformers' dequant as it was.
  • The block-FP8 dense layout: the loader refuses a plan that shards the dense part (test_ep_dense_sharding.py); the decision replicates it, or refuses an explicit fsdp, naming MODELOPT_FSDP_SIZE=1 (test_resolve_distribution_plan.py).
  • Per-tensor FP8 refused before anything loads and read as fp8-per-tensor (test_ep_loader.py; both rows fail without the fix); the block-FP8 configs in the tests carry their weight_block_size.
  • Locally on this revision, the stack rebased on main c6fa613: pre-commit over the whole PR against main, at every PR of the stack; and at the stack's tip the full tests/unit on transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.
  • The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.
  • On GPUs: DeepSeek-V3 (block FP8) loads, calibrates and exports expert-parallel through this path (see [OMNIML-6085] Run expert-parallel PTQ from hf_ptq.py #2754).
  • Re-cut on 2026-10-27 (see the note at the top): pre-commit, mypy included, passes at this head over the whole change against main, and the series' PRs merged together give a tree byte-identical to the former stack's tip. This PR's own unit tests pass at this head on CPU (torch 2.14, transformers 5.18); CI runs the full unit suite.
  • Review fixes, 2026-10-27: a dequant dtype the config names as a string is read as the dtype it names; the test file's dead Fp8Dequantize skip is gone. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
  • Review fixes, 2026-10-27, round 2: the block-FP8 case of the replicated-dense-part test expects the out-of-memory warning [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742 now gives when the plan replicates a dense part it cannot shard. Pre-commit, mypy included, and this PR's own unit tests pass at this head.
  • Review fixes, 2026-10-27, round 3: the pre-5.10.1 dequant override takes the destination dtype the loader passes (5.10.1 is the first release to pass one); a partial-block weight is padded in its own fp8 dtype; the 5.10.1 wrapper does not wrap itself on a re-import; a test pins that transformers' config merge keeps the checkpoint's weight_block_size; a comment says why the block-fp8 dense rule applies under expert parallelism only; the per-tensor FP8 refusal names the missing weight_block_size. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
  • Review fixes, 2026-10-27, round 4: a per-tensor FP8 checkpoint whose config comes in as transformers' FP8 config object is refused too, its block read from the checkpoint's own config.json; for a block-fp8 checkpoint, the several-replicas remedy names only MODELOPT_FSDP_SIZE=1; both dequant patches check a weight's dims before unpacking its shape, and the pre-5.10.1 override drops an FP4 branch it could not run. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
  • Review fixes, 2026-10-27, round 5: an FP8 config object whose checkpoint has no readable config.json (a Hub ID not yet made local) is refused rather than read as 128x128 blocks; for a block-fp8 dense part within several replicas the remedy also names starting from an unquantized copy. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
  • Rebased on 2026-10-27 onto main 7c3c868, after [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739 merged: pre-commit, mypy included, and this PR's own unit tests pass at this head, and the series' PRs merged together add no unit-test failure over main (tests/unit/torch utils, export, quantization and models, on transformers 5.18 and 5.5.4 and on torch 2.8).

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive; runs only on the expert-parallel load of a block-FP8 checkpoint.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. The feature's entry lands with [OMNIML-6085] Run expert-parallel PTQ from hf_ptq.py #2754.
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 11 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2742.

Summary by CodeRabbit

  • New Features
    • Added support for loading models from native block-FP8 checkpoints, converting weights to the model’s configured dtype during loading.
    • Improved handling of partial or empty scale grids, varied tensor shapes, and per-expert weights.
    • Added compatibility with older Transformers versions while preserving built-in behavior in newer versions.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 3
  • main
  • release/.*
  • feature/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

The loader now configures dequantization for native FP8 checkpoints. For Transformers versions before 5.10.1, a compatibility patch handles scale grids and expert merging. Unit tests cover dequantization cases, patch behavior, and version gating.

Changes: FP8 checkpoint loading

Layer / File(s) Summary
FP8 dequantization and validation
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_fp8_dequant_patch.py
Adds dequantization support for partial and empty scale grids on Transformers versions before 5.10.1. Tests compare varied block shapes and output dtypes with reference results, and verify that newer versions bypass the patch.
Expert conversion and patch lifecycle
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_fp8_dequant_patch.py
Combines FP8 dequantization with expert merging to preserve per-expert indexing. Tests cover converter ordering, repeated patching, hook wrapping, and restoration.
FP8 loader configuration
modelopt/torch/utils/plugins/model_load_utils.py
Configures FP8 checkpoint loading with FineGrainedFP8Config(dequantize=True), applies the patch with the model dtype, and logs the selection. The MXFP4 configuration remains the alternative branch.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant load_and_prepare_ep
  participant from_pretrained
  participant CompatibilityPatch
  load_and_prepare_ep->>from_pretrained: load FP8 checkpoint with FineGrainedFP8Config(dequantize=True)
  load_and_prepare_ep->>CompatibilityPatch: apply patch with model dtype
Loading
Merge Risk: Moderate · previous review 78b38

Loading block-FP8 checkpoints with older Transformers versions can silently produce incorrectly scaled weights in edge cases. Those weights would then be used for calibration and export. Before merging, make the patch reject scale grids that do not match their weights, or document why the current fallbacks are valid.

Pre-merge checks: Passed 5
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed No listed security anti-pattern was introduced. The PR changes only modelopt/torch/utils/plugins/model_load_utils.py and a unit test; no modelopt/example dependency manifests changed. The added code…
Title check Passed The title names the main change: block-FP8 checkpoints are dequantized as they load under expert-parallel loading. The raw summary and PR objectives both center on the FP8 dequantization patch in load…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

No listed security anti-pattern was introduced. The PR changes only modelopt/torch/utils/plugins/model_load_utils.py and a unit test; no modelopt/example dependency manifests changed. The added code contains no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec comment. Existing trust_remote_code handling remains caller-configurable with a default of False.

Full details: Title check

Explanation

The title names the main change: block-FP8 checkpoints are dequantized as they load under expert-parallel loading. The raw summary and PR objectives both center on the FP8 dequantization patch in load_and_prepare_ep, which matches the title. The title is specific, uses the ticket tag, and is a single sentence of 8 words.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.86%. Comparing base (aea6150) to head (adc762a).

Files with missing lines Patch % Lines
modelopt/torch/utils/plugins/model_load_utils.py 93.54% 6 Missing ⚠️
Additional details and impacted files
@@                            Coverage Diff                            @@
##           shengliangx/hfep-6b-ep-dense-sharding    #2743      +/-   ##
=========================================================================
+ Coverage                                  61.83%   61.86%   +0.03%     
=========================================================================
  Files                                        649      649              
  Lines                                      72969    73059      +90     
=========================================================================
+ Hits                                       45117    45195      +78     
- Misses                                     27852    27864      +12     
Flag Coverage Δ
unit 61.86% <93.54%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from 6eab845 to d6cb219 Compare October 12, 2026 02:24
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from f354b3b to ed1ebbf Compare October 12, 2026 02:35
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2743/

Built to branch gh-pages at 2026-10-27 00:04 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test ed1ebbf

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from d6cb219 to a5aecef Compare October 12, 2026 08:39
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from ed1ebbf to 6db2eb0 Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 6db2eb0

@shengliangxu shengliangxu changed the title [8/19] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [8/19] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from a5aecef to e8384c3 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from 6db2eb0 to 6bcb1ca Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from e8384c3 to ffbb850 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from 6bcb1ca to 14f1610 Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 14f1610

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6062] [8/19] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [8/20] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from 14f1610 to f647855 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6062] [8/20] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [9/21] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test f647855

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from f647855 to a40c380 Compare October 14, 2026 00:14
@shengliangxu
shengliangxu added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6062] [9/21] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [10/22] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 14, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from a40c380 to d407931 Compare October 14, 2026 01:47
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test b62a4c8

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 2d8cc5d

A native block-FP8 checkpoint (e.g. DeepSeek-V3) is dequantized to bf16
while it loads, through transformers' FineGrainedFP8Config, since
quantizers attach to bf16 weights. Its dequant is corrected on
transformers < 5.10.1 for weights whose last 128x128 block is partial, and
made to emit the model's dtype.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…out the session

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The patch and its module flags sit in model_load_utils with the
expert-parallel loader that applies them, so the test drives and resets
them there.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The patch fuses the Fp8Dequantize that transformers prepends to the
routed-expert converter into its MergeModulelist, so the loader still
gives each expert its tensor_idx. Only the install was tested. The new
test runs the patched update_weight_conversions on an expert converter
and the resulting chain on per-expert block-FP8 weights. It fails if the
converter no longer leads with a MergeModulelist, or if the fused op
skips the dequant or the merge. transformers 5.18's chain passes an
output_dtype the patched dequant does not take, so the chain runs that
version's own dequant; the patched arithmetic has its own tests.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
- The two block-size cases, a grid finer than the configured block and
  no block configured, become rows of the blockwise-reference table, which
  gains configured and grid columns.
- The dtype test keeps its unrequested (bf16) and requested (fp16) cases;
  requesting bf16 or fp32 took the same branch.
- The version test keeps the 5.10.1 boundary; 5.17.0 took the same branch.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The pre-5.10.1 dequant patch emitted a non-empty weight unscaled when its scale
grid was empty. That only happens when a weight and its scales were sliced
apart, as transformers 5.9's expert tensor_idx bug did on expert-parallel loads,
so it loaded wrong weights without a word; upstream's own dequant fails on
such a grid. It also fell back to the configured block for a grid neither that
block nor an even split gives, scaling rows wrongly. Raise in both cases; an
empty weight still passes through its empty grid.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From 5.10.1 the patch kept transformers' own dequant untouched, on the
assumption that no shipping model has a partial last block there.
DeepSeek-V3, V3.1 and Kimi-K2-Instruct do: kv_a_proj_with_mqa has 576
rows under a 5-row grid of 128-row blocks, and Fp8Dequantize still
raises "not divisible by scale grid" for it, so their block-FP8 loads
failed. The stock dequant is kept, for its ue8m0 and FP4 decoding and
output_dtype, and wrapped: an FP8 weight whose grid is the configured
block's ceil grid is padded to whole blocks for it and cropped back.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
When fusing transformers' prepended expert dequant into its merge
failed, the patch printed a warning and went on. Every routed expert
then loaded sliced apart from its scales. The failure now raises. A
transformers without the update_weight_conversions hook prepends no
dequant, so there is nothing to fuse and the patch skips it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…tizes

The load dequantizes native block-FP8 checkpoints as it loads, so they
join the formats it accepts.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…heck

The hub-ID test now loads an unquantized, a block-FP8 and an MXFP4
config: each must reach the checkpoint's resolution, past the format
check, so dropping either format from the accepted set fails it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From transformers 5.10.1 the expert-parallel load wraps transformers' block-FP8 dequant, which derives
the block size from the scale grid, and padded a weight to whole blocks only when it did not divide
into that grid. A partial last block can still divide evenly: 448 rows under a 4-row grid of
128-row blocks go to the dequant as blocks of 112, and every row group after the first gets the
wrong scale, silently. The wrapper now pads whenever the weight is not block-aligned, which is a
no-op for whole blocks.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The pre-5.10.1 dequant multiplies its own fp32 copy of the weight by the tiled scales in place,
so a weight holds two full-size fp32 temporaries instead of three while it loads. The dequant is
rebound after the expert converter's fuse, so a refused patch leaves transformers as it was.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The rule that a block-FP8 checkpoint's dense part is not sharded lands here, with the load that
makes such a checkpoint loadable, rather than in [10/21], where it preempted the format refusal.
Left to decide, the plan replicates that dense part and says why; asked to shard it, the decision
refuses, naming MODELOPT_FSDP_SIZE=1, and the loader refuses a plan that shards it.

The pre-5.10.1 dequant scales its fp32 copy of a weight in place, through a view of its whole
column blocks and then the partial last one, against scales tiled over the rows only. It builds no
full-size fp32 scale tile: an (18432, 7168) weight peaks at 6 bytes per element instead of 8. A
weight narrower than one block joins the reference table.

The expert converter's fuse no longer skips a merge it already fused, a layout transformers'
update_weight_conversions never produces (it builds fresh converters on every call); fusing it
again would dequantize once all the same. The load's log names the dtype the dequant emits.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
quant_method "fp8" is not block FP8 alone: per-tensor FP8 checkpoints (AutoFP8's: a scalar
weight_scale per weight, no weight_block_size) name the same method. The expert-parallel load
admitted them, and transformers' dequant reads them as 128x128 blocks with no weight_scale_inv, so
their weights load unscaled without an error (on transformers 5.18 a tiny Llama loaded that way
has max |w| 448 where the original has 0.079). _ckpt_quant_method now calls an fp8 checkpoint
without a weight_block_size "fp8-per-tensor", which the load refuses, before anything loads, as a
format it cannot dequantize; the decision's block-FP8 rule no longer applies to it either.

The decision, the loader's refusal and its docstring give the reason a block-FP8 dense part cannot
be sharded as Distributed Checkpoint reading it as stored, with no dequantization: its scales'
format was never the blocker (DeepSeek-V3's are fp32). A comment says where the dequant's block size
comes from: transformers keeps the checkpoint's quantization_config and takes only the loading
attributes from the one the load passes.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A config the load is handed can name its dtype, or carry "auto" for the checkpoint's own, and the
dequant's Tensor.to reads a string as a device. The dtype a string names is now taken as the torch
dtype, and "auto" as the bfloat16 default the dequant emits unrequested.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…tize

Every transformers the repo supports ships Fp8Dequantize (5.5 does), so the skip never fires, and
the load dereferences the class on the same releases.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The override installs below transformers 5.10.1 and took exactly (quantized, scales): 5.8.0 to
5.10.0 call it that way, and 5.10.1, where the gate switches to transformers' own dequant, is the
first to pass output_dtype. A caller passing it would have stopped the load with a bare TypeError.
It now takes output_dtype and, when given, emits it: the loader passes the destination dtype, which
the module-level model dtype only reconstructs.

The expert-converter test runs the patched dequant through this transformers' chain, which passes
output_dtype, instead of restoring the stock one around it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…uant

From transformers 5.10.1 a partial last block is padded to whole blocks for transformers' own
dequant. The padding cast the weight to fp32 first, so the dequant got an input dtype it never sees
from the loader, and the load an fp32 copy it does not need. The weight is now padded through a
same-size uint8 view, in its own fp8 dtype; the zero byte is zero in every fp8 format.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From transformers 5.10.1 the patch wraps transformers' dequant, calling the method it replaces, and
only this module's flag kept it from wrapping again. Re-importing the module loses that flag, so the
wrapper wrapped itself, one frame per re-import. The dequant op now carries a flag of its own, as
the FP8 quantizer does for its converter hook.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load passes FineGrainedFP8Config(dequantize=True), and both dequant branches read
weight_block_size off the quantizer's config, relying on transformers to keep the checkpoint's
quantization_config and take only the loading attributes from the one passed. A test now pins that
merge on whichever transformers runs the suite.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Its reason, that Distributed Checkpoint reads block FP8 as stored, holds at any EP degree, but its
remedy, replicating the dense part to dequantize it as it loads, holds only where the
expert-parallel load runs. Without expert parallelism the FSDP2 load refuses a quantized checkpoint
itself.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ckpoint

An fp8 checkpoint without weight_block_size was told the load "cannot dequantize a 'fp8-per-tensor'
checkpoint" while listing fp8 among what it dequantizes, and the refusal never named the block size
that tells the two apart. It now says the load dequantizes block-fp8 checkpoints, which name their
block in quantization_config.weight_block_size, and that this one names none.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A per-tensor FP8 checkpoint names no weight_block_size, which is how the load tells it from block
FP8 and refuses it. transformers' FP8 config object fills in a 128x128 block whatever the
checkpoint names, so a per-tensor checkpoint whose config came in as that object, as a loaded
model's does, read as block FP8 and would load unscaled. For an FP8 config object, the block now
comes from the checkpoint's own config.json, where both the decision and the load can read it.

The plan decision also stops offering an EP degree for a block-fp8 dense part sharded within
several expert-data-parallel replicas: no EP degree lets that dense part shard.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Both patches unpacked the last two dims first, so a weight or scale grid of fewer than two dims
failed with a bare unpack error from inside the patched method. From 5.10.1 such a call now goes to
transformers' own dequant, as every case the patch does not place does; below it, the override
refuses it by name. The override also drops its FP4 branch: it called an unpack transformers only
added in 5.10.1, so it could not run on the releases the override replaces.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The per-tensor check reads an FP8 config object's block from the checkpoint's own config.json, but
a path without one, a Hub ID the load has not made local yet, left the object's default 128x128
block standing, and a per-tensor checkpoint would still load unscaled. Such a config is now
"fp8-unverified", which the load refuses, naming the two ways in: the config as
AutoConfig.from_pretrained reads it, or a local checkpoint. Reading the file decides it on every
rank alike, without fetching anything.

For a block-fp8 dense part within several expert-data-parallel replicas, the remedy also names
starting from an unquantized copy, which on the warning path is the one that changes the outcome.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test a460975

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review — 0 CRITICAL, 0 IMPORTANT, 0 new SUGGESTION

Full review (the trigger comment carried no scoping instructions). 5 files changed (803+/46−). I read modelopt/torch/utils/plugins/model_load_utils.py around every changed hunk plus _decide_distribution whole, the resolve_distribution_plan signature/docstring, load_and_prepare_ep's signature and its refusal block at :2438-:2466, and all four test diffs.

State of the change

HEAD is a4609752 ("Refuse an FP8 config object whose checkpoint config cannot be read") — the round-5 revision, and the metadata matches byte-for-byte what the last round approved. Nothing has moved but the base. I did not take the prior rounds' verification on trust; the re-derivation below is independent.

Re-derived this round

  • Pre-5.10.1 block arithmetic holds on every boundary I could construct. whole = cols // block_n can never exceed scale_cols: the ceil-at-config branch of _blk gives floor(cols/block) <= ceil(cols/block), the exact-division branch gives equality — so s[..., :whole, None] is always in range and the tail is 0 or 1 scale columns, which broadcasts against a remainder narrower than block_n. whole == 0 (a weight narrower than one block) unflattens a zero-width slice, which is legal, and the tail mul_ then covers the whole weight. scale_rows * block_m >= rows in both branches, so [..., :rows, :] never under-fills the row tile. q[..., :whole*block_n] is basic slicing and unflatten(-1, (whole, block_n)) on strides (…, cols, 1) is view-expressible, so the mul_ really is in place on q's own copy=True buffer — the checkpoint tensor is untouched, and the PR's measured 6.0 B/elem matches q (fp32) + the bf16 result with no full-size fp32 tile.
  • The >= 5.10.1 bypass condition is still the exact one under which stock recovers the configured block. Padding fires only when scales.shape[-2:] == ceil_grid and the weight is not block-aligned; after the pad, upstream's padded_rows // scale_rows is block[0] by construction. Gating on grid divisibility instead — the shape this had in an earlier round — is what the (192, 256) / (2, 2) case pins against. The pad is done in the weight's own fp8 dtype through an equal-itemsize uint8 view, where F.pad's constant zero is also fp8 zero, and fp8_dtypes excludes the FP4 dtypes so upstream's unpack and ue8m0 decoding are never routed through it.
  • Refusal ordering and reachability. _ckpt_quant_method has exactly two call sites (:1470, :2438), so neither "fp8-per-tensor" nor "fp8-unverified" can reach a stale == "fp8" comparison; the four quant_method checks at :2439/:2444/:2452/:2459 fire in the right order, each with a distinct and actionable message, and all of them precede _resolve_checkpoint_dir — a refused Hub-ID load downloads nothing. block_fp8 deliberately does not match the two new values, so such a plan can still resolve to dense="fsdp"; harmless, since the format refusal is reached either way before a weight is read.
  • _decide_distribution composition. block_fp8 is read after ep_size is resolved from "auto", and from the deepcopy that build_meta_causal_lm has already written to — _ckpt_quant_method handles quantization_config as dict or object, so a dict→object normalization during the build cannot flip the answer. The elif arm sets unshardable/remedy that only dense == "auto" (warn + replicate) or dense == "fsdp" (raise) consume, so an explicit "replicate" is a no-op. MODELOPT_FSDP_SIZE=1 really does resolve to dense="replicate", and the new test row (8 experts, world 4 → ep_size == 4, edp == 1) lands on the fp8 elif rather than the edp arm, so it pins the branch it means to.
  • No subfolder hole. load_and_prepare_ep takes no subfolder, so _config_json_quantization(ckpt_path) reading ckpt_path/config.json is consistent with what that call site can be handed; at the _decide_distribution site resolve_distribution_plan has already resolved the snapshot and joined subfolder. Its failure modes are all caught — FileNotFoundError/NotADirectoryError under OSError, JSONDecodeError under ValueError.
  • Patch hygiene, re-imports, collectives. Both install flags live on the transformers classes, not the module, so a re-imported model_load_utils cannot double-wrap; the pre-5.10.1 override is a replacement rather than a wrapper, so a rebind after a lost module flag is idempotent; the dequant rebind sits after the converter fuse, so a refused fuse leaves transformers stock (asserted over all three patch targets); and the fuse collapses [Fp8Dequantize, _DequantThenMerge, Concatenate] back to one op instead of skipping and leaving operations[0] an Fp8Dequantize. Every refusal is config-derived and so raises identically on every rank; the is_master() print is the only rank-dependent statement and issues no collective. No mode registration, modelopt_state schema, public __init__ export or export path is touched, and transformers/packaging imports are all function-local.
  • Dtype normalization. _patch_finegrained_fp8_dequant resolves a string dtype (and "auto" → bf16) before it can reach Tensor.to, which would otherwise parse it as a device; output_dtype takes precedence over the module global, which is what keeps two sequential loads at different dtypes from borrowing each other's.

Findings

No new CRITICAL or IMPORTANT issues. The two non-blocking suggestions from the previous round are still open, unchanged, and I am not re-filing them as new threads:

  1. :2455 — the block verified in config.json is not plumbed into FineGrainedFP8Config(dequantize=True). Narrower than it reads: a caller-supplied config object with a non-default weight_block_size survives transformers' merge, so this only bites a caller who hands a default-block object for a non-128 checkpoint.
  2. :2098 — _config_json_quantization returns the first quantization_config dict without checking its quant_method or which sub-config the caller was examining, so the per-tensor refusal text can describe AutoFP8 for a checkpoint naming another format. Refuses either way; only the message misleads.

Risk

Low. Additive, and reached only by an expert-parallel plan over a block-FP8 checkpoint, with no in-repo caller until #2754. On transformers >= 5.10.1 every weight but the partial-block ones goes to upstream unchanged; the pre-5.10.1 override is checked against an exact blockwise reference across 10 parametrized shapes with visible mutation-checking, and the expert-converter fuse is exercised on a real update_weight_conversions output rather than a mock. Test weight matches the repo's "be stingy" guidance — the shared reference table carries the load, the pre-existing _ckpt_quant_method and hub-ID cases were parametrized rather than duplicated, and the new tmp_path case is a three-row table over one file write. All CPU-only and fast, so tests/unit's 20-minute budget is unaffected. Approving.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants